package cronCreationAndParse.cronParse.parseNextTime;



import cronCreationAndParse.dateProcess.StringUtils;

import java.util.*;

//定义CronSequenceGenerator类
public class CronSequenceGenerator implements CronParse {
    //属性
    private final String expression;  //cron表达式
    //在操作 Date, Calendar等表示日期/时间的对象时，经常会用到TimeZone；因为不同的时区，时间不同。
    //用来定位时区，获取精确的时间
    private final TimeZone timeZone; //TimeZone 表示时区偏移量，也可以计算夏令时,

    //BitSet类实现了一个按需增长的位向量。位Set的每一个组件都有一个boolean值。用非负的整数将BitSet的位编入索引。可以对每个编入索引的位进行测试、设置或者清除。
    //每个位 set 都有一个当前大小，也就是该位 set 当前所用空间的位数。注意，这个大小与位 set 的实现有关，所以它可能随实现的不同而更改。位 set 的长度与位 set 的逻辑长度有关，并且是与实现无关而定义的。
    //月份一共12个月，这部分主要是用来定义符合要求的月份
    private final BitSet months = new BitSet(12);  //一个Bitset类创建一种特殊类型的数组来保存位值。
    //用来定义符合要求的“day”
    private final BitSet daysOfMonth = new BitSet(31);
    //定义符合cron要求的"week"
    private final BitSet daysOfWeek = new BitSet(7);
    //定义符合要求的"hours"
    private final BitSet hours = new BitSet(24);
    //定义符合要求的"minutes"
    private final BitSet minutes = new BitSet(60);
    //定义符合要求的“seconds”
    private final BitSet seconds = new BitSet(60);

    //用列表中的索引替换逗号分隔列表中的值(不区分大小写)。
    //替换列表中的值的新字符串
    private String replaceOrdinals(String value, String commaSeparatedList) {
        String[] list = StringUtils.commaDelimitedListToStringArray(commaSeparatedList);
        for (int i = 0; i < list.length; i++) {
            String item = list[i].toUpperCase();
            value = StringUtils.replace(value.toUpperCase(), item, "" + i);
        }
        return value;
    }
    //设置符合cron要求中的"day"
    private void setDaysOfMonth(BitSet bits, String field) {
        int max = 31;
        // Days of month start with 1 (in Cron and Calendar) so add one
        setDays(bits, field, max + 1);
        // ... and remove it from the front
        bits.clear(0);
    }

    private void setDays(BitSet bits, String field, int max) {
        //将day部分的?替换成*
        if (field.contains("?")) {
            field = "*";
        }
        setNumberHits(bits, field, 0, max);
    }
    private void setMonths(BitSet bits, String value) {
        int max = 12;
//        转换FOO这类的字符串为对应的索引???
        value = replaceOrdinals(value, "FOO,JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC");
        //JAVA中BitSet就是“位图”数据结构，根据“位图”的语义，数据的存在性可以使用bit位上的1或0来表示；一个bit具有2个值：0和1，正好可以用来表示false和true
        BitSet months = new BitSet(13);
        // Months start with 1 in Cron and 0 in Calendar, so push the values first into a longer bit set
        //月份在Cron中以1开始，在Calendar中以0开始，因此先将值放入较长的位集中
        //月份的最小值是1，最大值是12
        setNumberHits(months, value, 1, max + 1);
        // ... and then rotate it to the front of the months
        //．．． 然后把它转到月的前面
        for (int i = 1; i <= max; i++) {
            if (months.get(i)) {
                bits.set(i - 1);
            }
        }
    }

    private void setNumberHits(BitSet bits, String value, int min, int max) {
        String[] fields = StringUtils.delimitedListToStringArray(value, ",");
        for (String field : fields) {
            //不是间隔类型的
            if (!field.contains("/")) {
                //不是一个增量，所以它必须是一个范围(可能为空)
                // Not an incrementer so it must be a range (possibly empty)
                int[] range = getRange(field, min, max);
                bits.set(range[0], range[1] + 1);
            }
            else {
                //增量类型，是一个间隔
                String[] split = StringUtils.delimitedListToStringArray(field, "/");
                if (split.length > 2) {
                    //长度大于2表示原field存在问题，这里抛出异常(非法参数异常)
                    throw new IllegalArgumentException("Incrementer has more than two fields: '" +
                            field + "' in expression \"" + this.expression + "\"");
                }
                //范围获取,第几秒开始
                int[] range = getRange(split[0], min, max);
                if (!split[0].contains("-")) {
                    //如果不包含"-"
                    range[1] = max - 1;
                }
                //存放增量结果
                int delta = Integer.valueOf(split[1]);
                if (delta <= 0) {
                    //增量结果必须大于0
                    //取值有误，抛出参数异常错误
                    throw new IllegalArgumentException("Incrementer delta must be 1 or higher: '" +
                            field + "' in expression \"" + this.expression + "\"");
                }
                //循环得到符合条件的各个时间点，从range[0]开始（起始时间点）,每次的间隔是delta，时间终点是range[1]
                for (int i = range[0]; i <= range[1]; i += delta) {
                    bits.set(i);
                }
            }
        }
    }
    //判断cron表达式分割之后的fields基本格式是否正确
    private static boolean areValidCronFields(String[] fields) {
        return (fields != null && fields.length == 6);
    }

    /*返回该field的起始时间和结束时间(cron表达式内的)
    * 获取范围，返回值是一个整数数组
    * int[0]表示起始点，也就是min
    * int[1]表示最大的时间，也即是max
    * */
    private int[] getRange(String field, int min, int max) {
        int[] result = new int[2];
        //全选的情况ALL
        if (field.contains("*")) {
            //起始时间就是min(如果是秒，起始点就是0)
            result[0] = min;
            //结束时间就是这个field的最后一个值(秒的话，终止点就是59,这里应该是统一按照-1处理（对于月份这种，在其他函数使用时，再加1）)
            result[1] = max - 1;
            return result;
        }
        //不是范围，也不是全选，那就是指定
        if (!field.contains("-")) {
            //此时开始和结束是同一个值
            result[0] = result[1] = Integer.valueOf(field);
        }
        else {
            //是个范围
            String[] split = StringUtils.delimitedListToStringArray(field, "-");
            if (split.length > 2) {
                //格式有错，抛出异常
                throw new IllegalArgumentException("Range has more than two fields: '" +
                        field + "' in expression \"" + this.expression + "\"");
            }
            //第一个值为起始时间
            result[0] = Integer.valueOf(split[0]);
            //第二个值为结束时间
            result[1] = Integer.valueOf(split[1]);
        }
        //范围时间格式检验
        if (result[0] >= max || result[1] >= max) {
            throw new IllegalArgumentException("Range exceeds maximum (" + max + "): '" +
                    field + "' in expression \"" + this.expression + "\"");
        }
        if (result[0] < min || result[1] < min) {
            throw new IllegalArgumentException("Range less than minimum (" + min + "): '" +
                    field + "' in expression \"" + this.expression + "\"");
        }
        if (result[0] > result[1]) {
            throw new IllegalArgumentException("Invalid inverted range: '" + field +
                    "' in expression \"" + this.expression + "\"");
        }
        //时间范围无误，返回
        return result;
    }

    /**
     * cron表达式解析函数
     * 这里会抛出参数非法异常
     * @param expression cron表达式
     *
     * */
    private void parse(String expression) throws IllegalArgumentException {
        String[] fields = StringUtils.tokenizeToStringArray(expression, " ");
        //判断分割之后fields的各个索引值格式是否正确
        if (!areValidCronFields(fields)) {
            throw new IllegalArgumentException(String.format(
                    "Cron expression must consist of 6 fields (found %d in \"%s\")", fields.length, expression));
        }
        //格式正确，进行解析
        doParse(fields);
    }

    private void doParse(String[] fields) {
        //对各个field进行设置
        setNumberHits(this.seconds, fields[0], 0, 60);
        setNumberHits(this.minutes, fields[1], 0, 60);
        setNumberHits(this.hours, fields[2], 0, 24);
        //调整月份和日期之间的关系
        //这里涉及到星期
        setDaysOfMonth(this.daysOfMonth, fields[3]);
        setMonths(this.months, fields[4]);
        //星期的修改
        setDays(this.daysOfWeek, replaceOrdinals(fields[5], "SUN,MON,TUE,WED,THU,FRI,SAT"), 8);
        if (this.daysOfWeek.get(7)) {
            // Sunday can be represented as 0 or 7
            this.daysOfWeek.set(0);
            this.daysOfWeek.clear(7);
        }
    }

    //构造器
    public CronSequenceGenerator(String expression) {
        this(expression, TimeZone.getDefault());
    }

    //构造器
    public CronSequenceGenerator(String expression, TimeZone timeZone) {
        this.expression = expression;
        this.timeZone = timeZone;
        parse(expression);
    }
    //构造器
    private CronSequenceGenerator(String expression, String[] fields) {
        this.expression = expression;
        this.timeZone = null;
        doParse(fields);
    }

    //从calendar开始寻找下一个匹配cron表达式的时间
    private void doNextNew(Calendar calendar) {
        //calendar中比当前更高的域是否调整过
        boolean changed = false;
        //创建一个列表,各个位置元素如下：月,天,小时,分钟,秒
        List<Integer> fields = Arrays.asList(Calendar.MONTH, Calendar.DAY_OF_MONTH,
                Calendar.HOUR_OF_DAY, Calendar.MINUTE, Calendar.SECOND);
        //依次调整月，日，时，分，秒
        //从高的field往下调整
        for (int field : fields) {
            if (changed) {
                //更高的域如果调整过，对当前域就需要进行判断
                //Calendar类中的set(int calndr_field，int new_val)方法用于将calndr_field值设置为new_val。此日历的旧字段将替换为新字段。
                //DAY_OF_MONTH 当月的第几天，从1开始
                //field值如果和对应当前日历的值一样，就赋值为1，否则赋值0
                calendar.set(field, field == Calendar.DAY_OF_MONTH ? 1 : 0);
            }
            if (!checkField(calendar, field)) {
                //当前域不符合cron表达式，需要进行调整
                changed = true;
                findNext(calendar, field);
            }
        }
    }
    //检查某个域是否匹配cron表达式
    private boolean checkField(Calendar calendar, int field) {
        switch (field) {
            case Calendar.MONTH: {
                //当前在月份部分
                //获取当前月份，但是月份值比实际月份小1
                int month = calendar.get(Calendar.MONTH);
                //get(int bit_in)方法用于返回给定位索引(bit_in)的值。当使用set()方法设置具有给定索引的位时，它返回true
                return this.months.get(month);
            }
            case Calendar.DAY_OF_MONTH: {
                //获取当前月份和星期
                int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH);
                int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK) - 1;
               //同时符合才算符合
                return this.daysOfMonth.get(dayOfMonth) && this.daysOfWeek.get(dayOfWeek);
            }
            case Calendar.HOUR_OF_DAY: {
                //小时
                int hour = calendar.get(Calendar.HOUR_OF_DAY);
                return this.hours.get(hour);
            }
            case Calendar.MINUTE: {
                int minute = calendar.get(Calendar.MINUTE);
                return this.minutes.get(minute);
            }
            case Calendar.SECOND: {
                int second = calendar.get(Calendar.SECOND);
                return this.seconds.get(second);
            }
            default:
                return true;
        }
    }

    //调整某个域到下一个匹配值，使其符合cron表达式
    private void findNext(Calendar calendar, int field) {
        switch (field) {
            case Calendar.MONTH: {
                //调整月份
                if (calendar.get(Calendar.YEAR) > 2099) {
                    //年份超出范围，抛出异常
                    throw new IllegalArgumentException("year exceeds 2099!");
                }
                //获取当前月份
                int month = calendar.get(Calendar.MONTH);
                //nextSetBit()方法用于检索在给定的src_in之后出现或开始搜索的设置为true的第一位的索引。
                int nextMonth = this.months.nextSetBit(month);
                //在本年中没有找到下一个符合要求的月份
                if (nextMonth == -1) {
                    //添加一年，在下一年中开始寻找
                    calendar.add(Calendar.YEAR, 1);
                    //设置月份从1月份开始，在calendar中索引为0
                    calendar.set(Calendar.MONTH, 0);
                    //再次寻找符合要求的月份
                    nextMonth = this.months.nextSetBit(0);
                }
                if (nextMonth != month) {
                    //月份发生了改变，就把月份赋值为新月份
                    calendar.set(Calendar.MONTH, nextMonth);
                }
                break;
            }
            case Calendar.DAY_OF_MONTH: {

                while (!this.daysOfMonth.get(calendar.get(Calendar.DAY_OF_MONTH))
                        || !this.daysOfWeek.get(calendar.get(Calendar.DAY_OF_WEEK) - 1)) {

                    //在使用calendar.getActualMaximum(calendar.DAY_OF_MONTH)获取每月最后一天时
                    int max = calendar.getActualMaximum(Calendar.DAY_OF_MONTH);
                    //选择下一个符合要求的天，在本月中
                    int nextDayOfMonth = this.daysOfMonth.nextSetBit(calendar.get(Calendar.DAY_OF_MONTH) + 1);
                    //在本月中没找到，
                    if (nextDayOfMonth == -1 || nextDayOfMonth > max) {
                        //对月份做出调整
                        calendar.add(Calendar.MONTH, 1);
                        //发现月份部分匹配的cron的月份
                        findNext(calendar, Calendar.MONTH);
                        //设置日期为1号
                        calendar.set(Calendar.DAY_OF_MONTH, 1);
                    } else {
                        //找到了符合条件的
                        calendar.set(Calendar.DAY_OF_MONTH, nextDayOfMonth);
                    }
                }
                break;
            }
            case Calendar.HOUR_OF_DAY: {
                //得到当前的小时
                int hour = calendar.get(Calendar.HOUR_OF_DAY);
                //得到下一个符合要求的小时
                int nextHour = this.hours.nextSetBit(hour);
                //没找到的话，在这一天里面
                if (nextHour == -1) {
                    //对天做出调整
                    calendar.add(Calendar.DAY_OF_MONTH, 1);
                    findNext(calendar, Calendar.DAY_OF_MONTH);
                    //把小时归零，重新寻找符合的小时
                    calendar.set(Calendar.HOUR_OF_DAY, 0);
                    nextHour = this.hours.nextSetBit(0);
                }
                if (nextHour != hour) {
                    //调整小时
                    calendar.set(Calendar.HOUR_OF_DAY, nextHour);
                }
                break;
            }
            case Calendar.MINUTE: {
                int minute = calendar.get(Calendar.MINUTE);
                int nextMinute = this.minutes.nextSetBit(minute);
                if (nextMinute == -1) {
                    calendar.add(Calendar.HOUR_OF_DAY, 1);
                    findNext(calendar, Calendar.HOUR_OF_DAY);
                    calendar.set(Calendar.MINUTE, 0);
                    nextMinute = this.minutes.nextSetBit(0);
                }
                if (nextMinute != minute) {
                    calendar.set(Calendar.MINUTE, nextMinute);
                }
                break;
            }
            case Calendar.SECOND: {
                int second = calendar.get(Calendar.SECOND);
                int nextSecond = this.seconds.nextSetBit(second);
                if (nextSecond == -1) {
                    calendar.add(Calendar.MINUTE, 1);
                    findNext(calendar, Calendar.MINUTE);
                    calendar.set(Calendar.SECOND, 0);
                    nextSecond = this.seconds.nextSetBit(0);
                }
                if (nextSecond != second) {
                    calendar.set(Calendar.SECOND, nextSecond);
                }
                break;
            }
        }
    }
    /**
     思路：  1、找到所有时分秒的组合并按照时分秒排序
     *      2、给定的时分秒在以上集合之前、之后处理
     *      3、给定时时分秒在以上集合中找到一个最小的位置
     *      4、day+1循环直到找到满足月、星期的那一天
     *      5、或者在列表中找到最小的即可
     */
    @Override
    public Date next(Date date) {
          /*  计划:
            1 .从整秒开始(如有必要，可以四舍五入)
            2如果秒匹配继续，否则寻找下一个匹配:
            2.1如果下一场匹配在下一分钟进行，则向前滚
            如果分钟匹配继续，否则查找下一个匹配
            3.1如果下一场匹配在一小时后进行，则向前滚动
            3.2重置秒数，请转到2
            如果小时匹配继续，否则查找下一个匹配
            4.1如果下一场匹配是在第二天，那么滚动向前，
            4.2重新设置分秒，执行2
            */
        Calendar calendar = new GregorianCalendar();
        //设置时区
        calendar.setTimeZone(this.timeZone);
        //设置时间:Calendar类中的setTime(Date dt)方法用于设置此Calendar的时间值表示的Calendars时间，并以给定或通过的日期为参数。
        calendar.setTime(date);
        // First, just reset the milliseconds and try to timesOfDay from there...
        //首先，只要重置毫秒，并尝试timesOfDay从那里…
        calendar.set(Calendar.MILLISECOND, 0);
        long originalTimestamp = calendar.getTimeInMillis();
        //得到下一个匹配cron表达式的时间
        doNextNew(calendar);
        if (calendar.getTimeInMillis() == originalTimestamp) {
//            我们得到了最初的时间戳——四舍五入到下一秒，然后再试一次。
            // We arrived at the original timestamp - round up to the next whole second and try again...
            calendar.add(Calendar.SECOND, 1);
            doNextNew(calendar);
        }
        return calendar.getTime();
    }
}